var SlidePuzzleEvent = function() { };

SlidePuzzleEvent.PIECE_MOVED = 'onPieceMoved';
SlidePuzzleEvent.PUZZLE_SOLVED = 'onPuzzleSolved';

var SlidePuzzle = function(context, img, numPieces)
{	
	// Constant variables
	var FRAMES_PER_SECOND = 24;
	
	if (context === null || !(context instanceof CanvasRenderingContext2D))
		throw new Error('Invalid context supplied. Expecting CanvasRenderingContext2D (' + context + ')');
	
	if (img === null || !(img instanceof HTMLImageElement))
		throw new Error('Invalid image supplied. Expecting HTMLImageElement (' + img + ')');

	if (numPieces < 4 || numPieces > 16 || numPieces % 4 !== 0)
		throw new Error('Invalid number of pieces. Minimum of 4, maximum of 16 and the number must be dividable by 4');
	
	var context = context;
	
	context.drawImage(img, 0, 0);
	
	var imageData = context.getImageData(0, 0, img.width, img.height),
		imageDataPixels = imageData.data;

	var pieces = [ ];

	var numRows = numPieces / 4,
		numPiecesPerRow = numPieces / numRows;
	
	var pieceWidth = img.width / numPiecesPerRow,
		pieceHeight = img.height / numRows;
	
	for (var y = 0, index = 0; y < numRows; y++)
	{
		for (var x = 0; x < numPiecesPerRow; x++)
		{
			var pieceX = pieceWidth * x,
				pieceY = pieceHeight * y,
				pieceData = context.getImageData(
								pieceX,
								pieceY,
								pieceWidth,
								pieceHeight
							);
			
			pieces.push(
					new PuzzlePiece(
						index++,
						x,
						y,
						pieceX,
						pieceY,
						pieceWidth, 
						pieceHeight,
						pieceData
					)
				);
		}
	}
	
	// Remove the last piece of the puzzle
	// and remeber the coordinates
	var p = pieces.pop(),
		emptyCol = p.col,
		emptyRow = p.row;

	/**
	 * Public functions that access private variables
	 */

	var eventListeners = [ ];

	this.addEventListener = function(event, listener)
	{
		eventListeners.push({
			event: event,
			func: listener
		});
	};	

	this.shuffle = function()
	{
		var numMoves = Math.ceil(Math.random() * 128),
			numPieces = pieces.length;
		
		for (var i = 0; i < numMoves; i++)
		{
			var moveablePieces = [ ];
			for (var j = 0; j < numPieces; j++)
			{
				if (canPieceMove(pieces[j]))
					moveablePieces.push(pieces[j]);
			}
			
			// Randomly move one of the pieces that are able to move
			movePiece(moveablePieces[Math.floor(Math.random() * moveablePieces.length)]);
		}
	};

	/**
	 * Private functions
	 */

	var fireEvent = function(event)
	{
		if (eventListeners.length === 0)
			return;
		
		for (var i = 0, len = eventListeners.length; i < len; i++)
		{
			if (eventListeners[i].event === event)
				eventListeners[i].func();
		}
	};

	/**
	 * Returns true if the piece is able to move either: right, left, up or down.
	 * This method'll return false in any other case.
	 */
	var canPieceMove = function(piece)
	{
		return (piece.col + 1 === emptyCol && piece.row === emptyRow) || 
			   (piece.col - 1 === emptyCol && piece.row === emptyRow) ||
			   (piece.row + 1 === emptyRow && piece.col === emptyCol) ||
			   (piece.row - 1 === emptyRow && piece.col === emptyCol);
	};

	var movePiece = function(piece)
	{
		// Unable to move the piece. Abort, abort!
		if (!canPieceMove(piece))
			return;
		
		var oldPieceIndex = piece.index;
		
		// Piece is able to move to the right
		if (piece.col + 1 === emptyCol && piece.row === emptyRow)
			++piece.index;
		
		// Piece is able to move to the left
		else if (piece.col - 1 === emptyCol && piece.row === emptyRow)
			--piece.index;
			
		// Piece is able to move up
		else if (piece.row + 1 === emptyRow && piece.col === emptyCol)
			piece.index += numPiecesPerRow;
		
		// Piece is able to move down
		else if (piece.row - 1 === emptyRow && piece.col === emptyCol)
			piece.index -= numPiecesPerRow;
		
		if (piece.index !== oldPieceIndex)
		{
			var oldPieceCol = piece.col,
				oldPieceRow = piece.row;
				
			piece.col = emptyCol;
			piece.row = emptyRow;
			
			// Set our new empty column and row to the
			// old position of the piece
			emptyCol = oldPieceCol;
			emptyRow = oldPieceRow;
			
			// Calculate a new x and y for the piece
			piece.x = piece.getWidth() * piece.col;
			piece.y = piece.getHeight() * piece.row;
			
			// We want to re-render the piece because 
			// its got a set of new coordinates
			piece.shouldUpdate = true;
			
			var solvedThePuzzle = true;
			for (var i = 0, len = pieces.length; i < len; i++)
			{
				var p = pieces[i];
				
				// If the index doesn't go from 0..1..n
				// then the pieces aren't in the correct
				// order yet
				if (p.index !== i)
				{
					solvedThePuzzle = false;
					break;
				}	
			}
			
			// We've moved a piece of the puzzle. Notify the
			// event listeners
			fireEvent(SlidePuzzleEvent.PIECE_MOVED);
			
			// The puzzle seems to be solved. Notify the 
			// event listeners about this great victory
			if (solvedThePuzzle)
				fireEvent(SlidePuzzleEvent.PUZZLE_SOLVED);	
		}
	};

	var render = function()
	{
		context.clearRect(0, 0, context.canvas.width, context.canvas.height);
		
		for (var i = 0, len = pieces.length; i < len; i++)
		{
			var piece = pieces[i];
			
			// Render the piece(s) that should be updated
			if (piece.shouldUpdate)
			{
				context.putImageData(
						piece.getImageData(),
						piece.x,
						piece.y
					);
			}
		}
	};

	/**
	 * Event listeners
	 */
	context.canvas.addEventListener('click', function(e) {

		var x = e.offsetX,
			y = e.offsetY;
		
		// Check if one of the pieces was clicked	
		for (var i = 0, len = pieces.length; i < len; i++)
		{
			var piece = pieces[i];
			
			// Did we click inside the bounds of a piece?
			if (piece.x <= x && piece.y <= y &&
			 	(piece.x + piece.getWidth()) >= x &&
				(piece.y + piece.getHeight()) >= y)
			{
			    movePiece(piece);
				break;
			}
		}

	});	
	
	// You can calculate the frames per second by dividing a second (1000)
	// by the frames (FRAMES_PER_SECOND)
	setInterval(render, 1000 / FRAMES_PER_SECOND);
	
};